Java HashMap 源码浅析 三

一. get()方法

    public V get(Object key) {
        Node<K,V> e;	//定义临时节点用于返回结果
        //先调用hash(key)方法获得key的hash值,再调用getNode方法找到Node节点
	    //如果Node节点不存在返回null,存在的话返回Node节点的value
        return (e = getNode(hash(key), key)) == null ? null : e.value;
    }

	//使用扰动函数使得hash值分布更加均匀
    static final int hash(Object key) {    	
        int h;
        return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
    }
	
	//根据hash和key查询Node节点
	final Node<K,V> getNode(int hash, Object key) {
        Node<K,V>[] tab; Node<K,V> first, e; int n; K k;
        //tab[(n - 1) & hash]可以找到key对应在hash桶中的位置
    	//如果hash桶不为空并且key对应hash桶的位置上存在节点,进入遍历
        if ((tab = table) != null && (n = tab.length) > 0 &&
            (first = tab[(n - 1) & hash]) != null) {
            
            //如果第一个节点的hash值和key值和当前查询的key一致,直接返回第一个节点
            if (first.hash == hash && // always check first node
                ((k = first.key) == key || (key != null && key.equals(k))))
                return first;
            //第一个节点的hash值或者key值和查询的key不一致,并且存在下一个节点,执行下面方法
            if ((e = first.next) != null) {
                if (first instanceof TreeNode)	 //如果当前节点在红黑树中
	                //调用getTreeNode方法获得红黑树中对应的节点
                    return ((TreeNode<K,V>)first).getTreeNode(hash, key);
                do {	//如果当前节点不在红黑树中,也就是链表中,进入循环
                	//如果循环中某个节点的hash值和key值一致,返回这个节点
                    if (e.hash == hash &&
                        ((k = e.key) == key || (key != null && key.equals(k))))
                        return e;
                } while ((e = e.next) != null);	//当不存在下一个节点的时候结束循环
            }
        }
        return null;	//如果hash桶为空或者key对应hash桶的位置上不存在节点,返回null
    }

获取流程:

  1. 计算需要查询的key的hash值
  2. 判断hash桶是不是空以及该hash值对应在hash桶上是否存在节点,不存在直接返回null
  3. 对比头节点的hash值和key是否和需要查询的key一致,如果一致直接返回头节点
  4. 判断头节点在红黑树中还是链表中
  5. 如果在红黑树中,则在红黑树中查找该节点
  6. 如果在链表中,则遍历链表查询该节点

下面来分析几个小细节:

1.先比较hash再比较key
我们发现判断HashMap中节点是否是需要查询的key的时候使用的判断条件是

e.hash == hash &&((k = e.key) == key || (key != null && key.equals(k)))

会先比较一下hash值是否一致再比较key是否相等,这么做是也是为了查询效率。
因为通常key都会重写equals方法,通过equals方法比较的效率明显是比hash值比较要慢的。
所以再equals判断之前先比较一次hash值可以提前规避很多不符合的节点,效率上会高一些。

2.先比较头节点,再遍历链表(红黑树)

我们发现当取到key对应hash桶中的节点的时候,会先比较一下头节点是否是我们需要查询的节点,如果不符,再去遍历链表(红黑树)。

因为在hash算法足够分散的情况下,很多hash桶的每一个桶中都只会存在一个节点,所以先比较第一个节点再去遍历也能带来效率上的提升。

二. remove()方法

    public V remove(Object key) {
        Node<K,V> e;	//定义临时节点
        //先调用hash(key)方法获得key的hash值
	    //调用removeNode方法删除Node节点
	    //如果Node节点不存在返回null,存在的话返回Node节点的value
        return (e = removeNode(hash(key), key, null, false, true)) == null ?
            null : e.value;
    }

	//根据hash和key,删除Node节点
    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;
        //当table不为空,并且hash对应的桶不为空时
        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))))
                node = p;	 //用node记录要删除的头节点
            else if ((e = p.next) != null) {	//头节点不是要删除的节点,并且头节点之后还有节点
                if (p instanceof TreeNode)	//头节点为树节点,则进入树查找要删除的节点
                    node = ((TreeNode<K,V>)p).getTreeNode(hash, key);
                else {	//头节点为链表节点
                    do {	//遍历链表
                    	 //hash值相等,并且key地址相等或者equals
                        if (e.hash == hash &&
                            ((k = e.key) == key ||
                             (key != null && key.equals(k)))) {
                            node = e;	 //node记录要删除的节点
                            break;
                        }
                        p = e;	 //p保存当前遍历到的节点
                    } while ((e = e.next) != null);
                }
            }
             //我们要找的节点不为空
            if (node != null && (!matchValue || (v = node.value) == value ||
                                 (value != null && value.equals(v)))) {
                if (node instanceof TreeNode)
                	//在树中删除节点
                    ((TreeNode<K,V>)node).removeTreeNode(this, tab, movable);
                else if (node == p)	 //我们要删除的是头节点
                    tab[index] = node.next;	//删除标题元素
                else	 //不是头节点,将当前节点指向删除节点的下一个节点
                    p.next = node.next;	//删除多元素链表节点
                ++modCount;
                --size;
                afterNodeRemoval(node);
                return node;
            }
        }
        return null;
    }

三. HashMap常见问题

  1. 为什么需要负载因子系数?
    存在负载因子的原因还在于减轻哈希冲突,例如,默认情况下初始存储桶为16,或者通过等待直到完整的16个元素被扩展,某些存储桶中可能有多个元素。因此,加载因子默认为0.75,也就是说,第13个元素的HashMap大小16(阈值= 0.75 * 16 = 12)将扩展为32

  2. 降低负载因子系数的作用?
    在构造函数中,设置较小的负载系数,例如0.5甚至0.25。
    如果存在一个长期存在的Map,并且密钥不是固定的,则可以适当地增加初始大小,减小负载因子,减少冲突的可能性,并减少寻址时间。交换时间也值得。

  3. 是否在初始化时定义容量?
    通过以上源代码分析,每次扩展都需要重新创建存储桶数组,链表,数据转换等,因此扩展成本仍然很高。如果可以准确地设置或估计初始容量(即使较大),则有时值得交换时间。

四. HashMap1.7和1.8改动

  1. 加入红黑树
    在JDK1.7中,当键的哈希值相同时,将形成哈希表。当链表上的节点数越来越多时,HashMap的查询效率降低,查询性能从O(1)变为O(n)。HashMap中的缺陷。
    JDK1.8中的HashMap添加了一个红黑树来弥补这一缺点。当链接列表上的节点数超过8个时,HashMap会将链接列表转换为红黑树结构。红黑树结构的引入将原来降低的查询性能从O(n)改进为O(lgn)。

  2. 为什么会选择8作为链表转红黑树的阈值?
    在负载因子默认为0.75时,单个hsah槽内元素个数为8的概率小于百万分之一,所以将7作为一个分水岭,当槽内元素等于7时不进行转换,当元素数量大于等于8时转换为红黑树,槽内元素小于等于6时转换回链表.

  3. 干扰哈希方法
    JDK 1.7中的扰动函数将干扰密钥四次,但在JDK1.8中仅扰动一次,并且密钥的hashCode值进行XOR运算。
    根据前一篇文章的分析,我们知道HashMap使用hash&(length-1)来获取节点的索引位置,该位置是哈希值的最后几位,因此只有低数位在操作中,高阶数字被忽略,但是哈希方法中的XOR操作使hashCode的每个位都参与获取索引位置的操作,因此哈希函数映射更均匀,并降低了哈希冲突的可能性。

JDK 1.7 和1.8HashMap扩容的区别

  1. JDK 1.7在扩容前会判断hash桶是否为空,如果为空会提前创建;JDK 1.8会在扩容时检查hash桶是否为空
  2. JDK 1.8在链表转移的时候引入了低位链表和高位链表进行转移,效率更高
  3. 在JDK 1.7中HashMap使用头插法在扩容期间执行元素传输,在多线程情况下,两个节点有可能形成一个闭环链表,这将导致在扩容期间造成链表的死循环。
    但是,在JDK 1.8中,HashMap使用尾插法来执行元素传输,每个节点的原始顺序不会被颠倒,从而避免出现死循环的情况,但1.8中的HashMap仍然是线程不安全的,因为在扩容中,HashMap首先指向新的节点数组,然后在进行元素传输,因此在多线程情况下,get方法可能获取不到值。

线程安全的map?

Hashtable是线程安全的,它的每个方法中都加入了Synchronize方法。当需要多线程操作的时候可以使用线程安全的ConcurrentHashMap。ConcurrentHashMap虽然也是线程安全的,但是它的效率比Hashtable要高好多倍。因为ConcurrentHashMap使用了分段锁,并不对整个数据进行锁定。
也可使用Collections.synchronizedMap();返回一个线程安全的map集合。

参考链接:
链接: HashMap源码分析,基于1.8对比1.7
链接: JDK1.8 HashMap源代码分析

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

香辣奥利奥

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

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

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

打赏作者

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

抵扣说明:

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

余额充值