JDK1.8 HashMap源码分析

目录

1.类的底层数据结构

 2.类结构

2.1 元素节点Node,v>

2.2 红黑树节点TreeNode,v>

2.3 构造函数

2.3.1 HashMap(int initialCapacity, float loadFactor)

2.3.2 HashMap(int initialCapacity)

2.3.3 HashMap()

2.4 put(K key, V value)

2.5 get(Object key)

2.6 resize()

2.7 remove(Object key)

总结:


本文基于JDK1.8 对HashMap的底层数据结构、方法实现原理进行分析。

1.类的底层数据结构

Java HashMap底层采用哈希表结构(数组+链表或红黑树)实现,集合了数组和链表的优点:

a.数组优点:可以通过数组下标快速获取元素,效率极高

b.链表有点:插入或删除不需要移动元素,只需修改节点的引用,效率极高

hashMap底层数据结构图:

 2.类结构
2.1 元素节点Node<K,V>

HashMap底层使用数组存储数据,数组的类型为Node<K,V>

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;
        }
    }

Node节点包含4个元素:

hash: 根据key通过hash()方法计算的hash值

key: 键值对的键

value:键值对的值

next: 表示当前节点的下一个节点

2.2 红黑树节点TreeNode<K,V>
static final class TreeNode<K,V> extends LinkedHashMap.Entry<K,V> {
        TreeNode<K,V> parent;  // red-black tree links
        TreeNode<K,V> left;
        TreeNode<K,V> right;
        TreeNode<K,V> prev;    // needed to unlink next upon deletion
        boolean red;

        TreeNode(int hash, K key, V val, Node<K,V> next) {
            super(hash, key, val, next);
        }
        ......
}

红黑树的节点使用TreeNode表示,当链表中的元素超过8个(TREEIFY_THRESHOLD)并且数组长度大于64(MIN_TREEIFY_CAPACITY)时,链表会转换为红黑树。

2.3 构造函数

HashMap包含的几个重要变量及常量:

//数组默认初始长度 16
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16
//数组最大长度,2的30次幂,即1073741824
static final int MAXIMUM_CAPACITY = 1 << 30;
//默认加载因子
static final float DEFAULT_LOAD_FACTOR = 0.75f;
//链表转红黑树的长度阈值
static final int TREEIFY_THRESHOLD = 8;
//红黑树转链表的长度阈值
static final int UNTREEIFY_THRESHOLD = 6;
//链表转化为红黑树时,数组容量必须大于等于64
static final int MIN_TREEIFY_CAPACITY = 64;

//HashMap使用数组存放数据,数组元素类型为Node<K,V>
transient Node<K,V>[] table;
//HashMap中键值对的个数
transient int size;
// 用于快速失败,由于HashMap非线程安全,在对HashMap进行迭代时,如果期间其他线程的参与导致HashMap的结构发生变化了(比如put,remove等操作),直接抛出ConcurrentModificationException异常
transient int modCount;
//数组扩容阈值,计算方式为 数组容量*加载因子。
 int threshold;
//加载因子
final float loadFactor;
2.3.1 HashMap(int initialCapacity, float loadFactor)
HashMap(int initialCapacity, float loadFactor)指定了数组初始容量和加载因子
public HashMap(int initialCapacity, float loadFactor) {
        //校验指定数组长度
        if (initialCapacity < 0)
            throw new IllegalArgumentException("Illegal initial capacity: " +
                                               initialCapacity);
        if (initialCapacity > MAXIMUM_CAPACITY)
            //指定数组长度大于数组默认最大值2^30,initialCapacity赋值为MAXIMUM_CAPACITY
            initialCapacity = MAXIMUM_CAPACITY;
        //校验加载因子,当加载因子<=0或者不是float类型时,抛出IllegalArgumentException异常
        if (loadFactor <= 0 || Float.isNaN(loadFactor))
            throw new IllegalArgumentException("Illegal load factor: " +
                                               loadFactor);
        this.loadFactor = loadFactor;
        //获取>=指定容量的最小的2的n次幂
        this.threshold = tableSizeFor(initialCapacity);
    }

使用该构造函数时,数组扩容阈值threshold 为指定容量的最小的2的n次幂只有调用put方法

threshold才会被赋值为:数组容量*加载因子。

2.3.2 HashMap(int initialCapacity)
HashMap(int initialCapacity)指定了数组初始容量,使用默认加载因子
public HashMap(int initialCapacity) {
        //指定数组长度,使用默认加载因子
        this(initialCapacity, DEFAULT_LOAD_FACTOR);
    }
2.3.3 HashMap()
HashMap()无参构造函数
public HashMap() {
        //设置加载因子为默认加载因子 0.75
        this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted
    }
2.4 put(K key, V value)
put(K key, V value) 指定键值对,插入元素
public V put(K key, V value) {
        //根据key调用hash()方法结算hash值
        return putVal(hash(key), key, value, false, true);
    }

static final int hash(Object key) {
        int h;
        //计算key的哈希值,当key为null,返回0,否则通过(h = key.hashCode()) ^ (h >>> 16)公式计算哈希值。
        //该公式通过hashCode的高16位异或低16位,得到哈希值,主要从性能、哈希碰撞角度考虑,不会造成因为高位没有参与下标计算从而引起的碰撞
        return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
    }

final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
                   boolean evict) {
        //如果数组为null或者长度为0,则进行初始化操作
        Node<K,V>[] tab; Node<K,V> p; int n, i;
        if ((tab = table) == null || (n = tab.length) == 0)
            n = (tab = resize()).length;
        //根据key的哈希值计算出key在数组中存放的下标位置,公式为(n-1)&hash
        if ((p = tab[i = (n - 1) & hash]) == null)
            //如果目标下标位置没有元素,创建新的Node节点,并插入
            tab[i] = newNode(hash, key, value, null);
        else {
            Node<K,V> e; K k;
            //如果目标位置已存在元素,并且已有元素的哈希值、key值和新插入元素的哈希值、key值匹配,将已有节点赋值给e
            if (p.hash == hash &&
                ((k = p.key) == key || (key != null && key.equals(k))))
                e = p;
            else if (p instanceof TreeNode)
                //如果目标位置已存在元素p,并且p节点为红黑树,则插入红黑树中
                e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
            else {
                //否则目标位置已存在元素p,并且p节点为链表结构,遍历链表,尾部插入
                for (int binCount = 0; ; ++binCount) {
                    //如果当前节点p的next节点为null
                    if ((e = p.next) == null) {
                        //创建新的node节点,赋值给当前节点p的next节点
                        p.next = newNode(hash, key, value, null);
                        //如果链表长度大于等于TREEIFY_THRESHOLD,则考虑转为红黑树。binCount从0开始,所以TREEIFY_THRESHOLD-1
                        if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
                            treeifyBin(tab, hash);
                        break;
                    }
                    //如果链表中节点的哈希值、key值和新插入元素的哈希值、key值是否匹配,将已有节点赋值给e
                    if (e.hash == hash &&
                        ((k = e.key) == key || (key != null && key.equals(k))))
                        break;
                    p = e;
                }
            }
            //如果e不为null,则数组中存在插入的key
            if (e != null) { // existing mapping for key
                //获取已存在节点的旧值
                V oldValue = e.value;
                if (!onlyIfAbsent || oldValue == null)
                    //将节点e的值替换为新值
                    e.value = value;
                afterNodeAccess(e);
                //返回旧值
                return oldValue;
            }
        }
        //修改次数递增
        ++modCount;
        //当键值对个数大于等于扩容阈值的时候,进行扩容
        if (++size > threshold)
            resize();
        afterNodeInsertion(evict);
        return null;
    }

put操作过程如下:

a.判断数组是否为null或者长度为0,是的话初始化数组(所以在创建HashMap时,并不会初始化数组)

b.通过(n - 1) & hash计算key在数组中存放的下标位置

c.如果目标下标位置没有元素,创建新的Node节点,并插入

d.如果目标下标位置不为空,分为下面三种情况:

   d.1 已有元素的哈希值、key值和新插入元素的哈希值、key值匹配,覆盖旧值

  d.2 该节点为红黑树,则插入红黑树中

  d.3 该节点为链表,遍历链表,尾部插入

e. 如果链表长度大于等于TREEIFY_THRESHOLD=8,则考虑转为红黑树。binCount从0开始,所以是binCount>=TREEIFY_THRESHOLD-1

f.判断HashMap元素个数是否大于等于threadhold(扩容阈值),是的话,进行扩容

2.5 get(Object key)

get(Object key) 获取指定key的元素

public V get(Object key) {
        Node<K,V> e;
        //获取指定key的元素,如果元素为null,返回null,否则返回元素的值
        return (e = getNode(hash(key), key)) == null ? null : e.value;
    }

final Node<K,V> getNode(int hash, Object key) {
        Node<K,V>[] tab; Node<K,V> first, e; int n; K k;
        //判断数组是否为空,并且长度长度是否大于0,目标索引位置下元素是否为空,是的话就直接返回null
        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) {
                //如果类型是红黑树,就从红黑树中查找
                if (first instanceof TreeNode)
                    return ((TreeNode<K,V>)first).getTreeNode(hash, key);
                do {
                    //否则就是链表,遍历链表查找目标元素
                    if (e.hash == hash &&
                        ((k = e.key) == key || (key != null && key.equals(k))))
                        return e;
                } while ((e = e.next) != null);
            }
        }
        return null;
    }
2.6 resize()

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;
        //扩容前数组长度大于0,当前数组有数据
        if (oldCap > 0) {
            //如果扩容前的数组长度大于MAXIMUM_CAPACITY,就不扩容了,返回扩容前数组
            if (oldCap >= MAXIMUM_CAPACITY) {
                //设置数组的阈值为Integer.MAX_VALUE
                threshold = Integer.MAX_VALUE;
                return oldTab;
            }
            //扩大新数组的容量为老数组容量的两倍,但必须小于MAXIMUM_CAPACITY,并且老容量大于等于 默认初始长度16
            else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
                     oldCap >= DEFAULT_INITIAL_CAPACITY)
                //扩大新数组的阈值为老数组阈值的两倍
                newThr = oldThr << 1; // double threshold
        }
        //当前数组没有数据,新数组的容量赋值为老数组的阈值,因为new HashMap(int initialCapacity, float loadFactor)时,initialCapacity赋值给了threadhold
        else if (oldThr > 0) // initial capacity was placed in threshold
            newCap = oldThr;
        else {               // zero initial threshold signifies using defaults
            //当使用无参构造方法 new HashMap()时,新数组的容量使用默认值 DEFAULT_INITIAL_CAPACITY 16,新数组的阈值为默认容量*默认加载因子
            newCap = DEFAULT_INITIAL_CAPACITY;
            newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
        }
        if (newThr == 0) {
            //当新数组的阈值为0时,计算新数组的阈值,公式为新数组的容量*加载因子,最大值为Integer.MAX_VALUE
            float ft = (float)newCap * loadFactor;
            newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
                      (int)ft : Integer.MAX_VALUE);
        }
        //将新数组的阈值赋值给threshold属性
        threshold = newThr;
        //创建一个新的数组,数组类型为Node,长度为新数组的容量值 newCap
        @SuppressWarnings({"rawtypes","unchecked"})
        Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
        //将新数组赋值给数组table属性
        table = newTab;
        if (oldTab != null) {
            //如果旧数组不为null,说明旧数组中有元素,遍历旧数组,将旧数组中的元素赋值到新数组
            for (int j = 0; j < oldCap; ++j) {
                //声明一个节点e,用来接收遍历的节点
                Node<K,V> e;
                if ((e = oldTab[j]) != null) {
                    //旧数组当前下标的元素不为null,将旧数组当前下标的元素赋值为null,便于GC
                    oldTab[j] = null;
                    if (e.next == null)
                        //如果当前元素的下一个节点为null,说明当前元素没有后继节点
                        //直接将当前元素插入到新数组的指定位置
                        newTab[e.hash & (newCap - 1)] = e;
                    else if (e instanceof TreeNode)
                        //如果当前元素为树形结构,
                        ((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
                    else { // preserve order
                        //否则当前元素为链表结构,遍历链表 ,将链表中的元素使用尾插法插入新的数组
                        //定义两个链表,低位链表:loHead: 低位的头节点 loTail:低位的尾节点
                        //高位链表:hiHead: 高位的头节点  hiTail:高位的尾节点
                        Node<K,V> loHead = null, loTail = null;
                        Node<K,V> hiHead = null, hiTail = null;
                        Node<K,V> next;
                        do {
                            next = e.next;
                            //当前节点的hash&oldCap==0,在新数组索引位置不变
                            if ((e.hash & oldCap) == 0) {
                                if (loTail == null)
                                    //低位尾节点为null,将当前节点赋值给低位头节点loHead,插入第一个低位元素
                                    loHead = e;
                                else
                                    //低位尾节点不为null,将当前节点赋值给loTail
                                    loTail.next = e;
                                loTail = e;
                            }
                            else {
                            //否则在新数组索引位置改变,索引位置为原索引+oldCap
                                if (hiTail == null)
                                    //高位尾节点为null,将当前节点赋值给高位头节点hiHead,插入第一个高位元素
                                    hiHead = e;
                                else
                                    //高位尾节点不为null,将当前节点赋值给loTail
                                    hiTail.next = e;
                                hiTail = e;
                            }
                        } while ((e = next) != null);
                        if (loTail != null) {
                            loTail.next = null;//将低位尾节点的下一个节点赋值为null
                            newTab[j] = loHead;//将低位链表的头节点插入新数组中,插入下标为原来元素所在的位置
                        }
                        if (hiTail != null) {
                            hiTail.next = null;//将高位尾节点的下一个节点赋值为null
                            newTab[j + oldCap] = hiHead;//将高位链表的头节点插入新数组中,插入下标为原来元素所在的位置+oldCap(旧数组的容量)
                        }
                    }
                }
            }
        }
        return newTab;
    }

扩容时,当节点为链表结构,旧数组中的元素在新数组中存放位置通过e.hash&oldCap结果是否为0来计算,主要有两种情况:

情况一:

扩容前oldCap=16,hash=1,则在旧数组中的存放位置 (n-1)&hash=15&1=1

扩容后newCap=32,hash=1,  则在新数组中存放的位置(n-1)&hash=31&1=1

存储位置没有不变,新的下标位置和原下标位置相同,hash&oldCap=1&16=0

情况二:

扩容前oldCap=16,hash=17,则在旧数组中存放的位置 (n-1)&hash=15&17=1

扩容后newCap=32,hash=17,则在新数组中存放的位置 (n-1)&hash=31&17=17

存储位置改变,新位置为原下标位置+oldCap=1+16=17,hash&oldCap=17&16=16

所以我们用公式 e.hash&oldCap是否为0来计算元素扩容后的新下标位置

2.7 remove(Object key)

remove(Object key) 删除指定元素

public V remove(Object key) {
        Node<K,V> e;
        //被删除直接key的节点是否为null,是null,直接返回,否则返回删除节点的value值
        return (e = removeNode(hash(key), key, null, false, true)) == null ?
            null : e.value;
    }

final Node<K,V> removeNode(int hash, Object key, Object value,
                               boolean matchValue, boolean movable) {
        Node<K,V>[] tab; Node<K,V> p; int n, index;
        //如果数组不为null,长度大于0,并且数组中指定key所在下标的元素不为空
        if ((tab = table) != null && (n = tab.length) > 0 &&
            (p = tab[index = (n - 1) & hash]) != null) {
            Node<K,V> node = null, e; K k; V v;
            if (p.hash == hash &&
                ((k = p.key) == key || (key != null && key.equals(k))))
                //当前节点的hash值、key值和要删除的指定key的hash值、key值匹配,把当前节点赋值给临时节点node
                node = p;
            else if ((e = p.next) != null) {
                if (p instanceof TreeNode)
                    //当前节点是树形结构,调用树形结构的方法getTreeNode()方法获取节点,并赋值给临时节点node
                    node = ((TreeNode<K,V>)p).getTreeNode(hash, key);
                else {
                    //否则节点为链表结构,遍历链表,找到匹配节点
                    do {
                        if (e.hash == hash &&
                            ((k = e.key) == key ||
                             (key != null && key.equals(k)))) {
                            //找到链表中匹配的节点,并赋值给临时节点node
                            node = e;
                            break;
                        }
                        //如果进入了链表中的遍历,那么此处的p不再是数组下标的节点,而是要删除结点的上一个结点
                        p = e;
                    } while ((e = e.next) != null);
                }
            }
            if (node != null && (!matchValue || (v = node.value) == value ||
                                 (value != null && value.equals(v)))) {
                if (node instanceof TreeNode)
                    //如果节点为树形结构,调用树形结构的方法removeTreeNode删除节点
                    ((TreeNode<K,V>)node).removeTreeNode(this, tab, movable);
                else if (node == p)
                    //如果要删除的节点为数组下标节点,也就是头结点,直接让下一个作为头
                    //要删除的节点为头节点,分为两种情况 1.节点为普通节点 2.节点为链表结构,头节点就是要删除的节点
                    tab[index] = node.next;
                else
                    //删除的节点在链表中,把要上一个结点next节点 指向 删除节点的next节点
                    p.next = node.next;
                //修改计数器
                ++modCount;
                //长度减一
                --size;
                afterNodeRemoval(node);
                //返回删除的节点
                return node;
            }
        }
        return null;
    }

remove()方法操作过程如下:

a.判断数组不为空、长度大于0,并且通过(n - 1) & hash计算key在数组中存放的下标位置元素不为空

b.当前节点的hash值、key值和要删除的指定key的hash值、key值匹配,赋值给临时节点node

c.当前节点的next节点不为空,分为下面两种情况:

   c.1 当前节点是树形结构,赋值给临时节点node

   c.2 该节点为链表,遍历链表,赋值给临时节点node

d.临时节点node为树形结构,调用树形结构删除方法

e.临时节点node为非树形结构,分为下面两种情况:

   e.1 如果要删除的节点为数组下标节点,也就是头结点,直接让数组当前位置指向临时节点node的next节点

   e.2 删除的节点在链表中,把要上一个结点next节点 指向 删除节点的next节点

总结:

HashMap代码写很巧妙,值得深读。

后续再单独写一篇解析HashMap关于红黑树操作、链表转红黑树、以及红黑树转链表的源码解析。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值