HashMap源码解析

一、哈希表

HashMap的本质就是一个哈希表,所以我们先来讨论一下哈希表。
首先,哈希表的主干是一个数组
既然哈希表的本质是一个数组,我们试试将存储的key映射为数组下标,存储value的值。那么我们将通过key定位到数组位置就需要一个函数,这个函数我们称之为哈希函数。假设一组数据{1,15},这组数据的值既是key也是value,哈希函数为f(x)=x%4。插入情况如下图所示:
在这里插入图片描述
哈希冲突
通过这个图我们很容易联想到,如果插入的两个数对映的映射相同怎么办?
这就是哈希冲突,一般来说我们有两种解决方案:1、将相同映射往数组后面移动,直到找到空的位置。
2、采用链表+数组的组合,每个数组上的元素都作为头节点,映射相同的话,就作为该元素在链表中指向的下一节点。
在哈希表中进行添加,删除,查找等操作,性能十分之高,不考虑哈希冲突的情况下,仅需一次定位即可完成,时间复杂度为O(1)。不难想象,冲突发生的越少,查找效率就越高,所以我们设计哈希函数的时候要尽量避免冲突

二、HashMap实现原理

HashMap采用链表+数组的组合,其有两个重要参数:初始容量加载因子
初始容量:哈希表创建时的容量。
加载因子:当前容量*加载因子=下次扩容前的容量。默认的加载因子是0.75,即数组存储的数据达到容量的四分之三的时候,下一次再插入时会进行扩容,默认扩容为当前容量的两倍。
HashMap的主干是一个table数组,存储的是Node类的对象:

transient Node<K,V>[] table;
 static class Node<K,V> implements Map.Entry<K,V> {
        final int hash;       //对key的HashCode值进行一定运算后的值
        final K key;
        V value;
        Node<K,V> next; //指向下一个单链表的Node

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

可以看到每个Node类中,都有一个key-value对。我们很容易直到,链表的存在是为了解决冲突,但因为存在链表,在一定程度上会降低我们查找的速度,因为可能无法一次定位的指定的数。所以虽然设计了链表,但是链表还是越少越好,越短越好。
我们再来看看几个重要参数:

transient int size;			//当前存储的key-value键值对个数
transient int modCount;		//HashMap进行操作的次数,如put。这个属性存在的原因是为了处理并发问题。
int threshold;				//阈值,初始为16,之后为capacity*loadFactory。
final float loadFactor;		//负载因子

我们先来看看HashMap的其中一个构造器。

public HashMap(int initialCapacity, float loadFactor) {
		//对初始容量进行校验,MAXIMUM_CAPACITY=1<<30。
        if (initialCapacity < 0)
            throw new IllegalArgumentException("Illegal initial capacity: " +
                                               initialCapacity);
        if (initialCapacity > MAXIMUM_CAPACITY)
            initialCapacity = MAXIMUM_CAPACITY;
        if (loadFactor <= 0 || Float.isNaN(loadFactor))
            throw new IllegalArgumentException("Illegal load factor: " +
                                               loadFactor);
        this.loadFactor = loadFactor;
        //使数组长度一定为2的次幂
        this.threshold = tableSizeFor(initialCapacity);
    }

这个构造器设置了初始容量和负载因子。代码也很好理解,不多做解释。
接下来我们看看put操作,这个操作是构建HashMap的核心。

public V put(K key, V value) {
    return putVal(hash(key), key, value, false, true);
}
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
                   boolean evict) {
        //为数组table分配空间
        Node<K,V>[] tab; Node<K,V> p; int n, i;
        if ((tab = table) == null || (n = tab.length) == 0)
            n = (tab = resize()).length;
        //(n-1)&hash:对hash值进行进一步计算,为确保散列均匀
        //如果当前位置不存在,那么就将Node分配在当前位置;如果存在
        if ((p = tab[i = (n - 1) & hash]) == null)
            tab[i] = newNode(hash, key, value, null);
        else {
            Node<K,V> e; K k;
            if (p.hash == hash &&
                ((k = p.key) == key || (key != null && key.equals(k))))
                e = p;
            else if (p instanceof TreeNode)
                e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
            else {
            //以数组中的Node为头节点,往后查询,当某下节点的下一节点为null时,执行插入操作。
                for (int binCount = 0; ; ++binCount) {
                    if ((e = p.next) == null) {
                        p.next = newNode(hash, key, value, null);
                        if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
                            treeifyBin(tab, hash);
                        break;
                    }
                    if (e.hash == hash &&
                        ((k = e.key) == key || (key != null && key.equals(k))))
                        break;
                    p = e;
                }
            }
            //如果对应key已经存在,那么就将旧的value用新的value替代,并返回旧的value
            if (e != null) { // existing mapping for key
                V oldValue = e.value;
                if (!onlyIfAbsent || oldValue == null)
                    e.value = value;
                afterNodeAccess(e);
                return oldValue;
            }
        }
        ++modCount;
        //如果这时后的size大于阙值,执行扩容操作。
        if (++size > threshold)
            resize();
        afterNodeInsertion(evict);
        return null;
    }

put操作中有一个非常重要的方法叫做resize(),这个操作的代码比较长,官方对它的解释是:“Initializes or doubles table size. If null, allocates in accord with initial capacity target held in field threshold. Otherwise, because we are using power-of-two expansion, the elements from each bin must either stay at same index, or move with a power of two offset in the new table.(初始化或者将table大小增加一倍。如果table为null,则根据字段阈值中保存的初始容量目标进行分配。否则,因为我们正在使用二次幂扩展,每个位置的元素必须和之前保持相同的索引,或者在新table中以2次幂偏移的方式移动)”
这样看起来事实上,扩容操作我们还是有点没有明白。我讲下我自己对这句话的理解。首先table的初始容量为16(2^4),之后我们每次以两倍的扩容,从二进制上去考虑,就是将那个0和1中的那个1向左移动过了一位。当进行扩容时,我们hash值与length-1进行&操作(至于为什么这么做之后进行讨论)。我们可以举个例子理解,我们将12和20分别插入容量为16和32的table中:

在这里插入图片描述在这里插入图片描述
我们可以看到,12不管是插入到容量为16还是32的table中,进行&操作后的结果都是01100。但20插入到容量为16的table中结果为00100,插入到容量为32的table中结果为10100。因为在进行扩容时,就是多出了最左边的一个1,只有一位的差异,如果我们的hash在该位为0,进行与操作后还是0,即最后与操作的结果相同,那么该Node扩容后的索引和扩容前相同,但如果我们的hash在该位为1,进行与操作后还是为1,那么最后我的操作结果会在左边多出一个1,相当于“在新table中以2次幂偏移的方式移动”。
我们再来看一下,hash是如何获得的:

static final int hash(Object key) {
        int h;
        return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
    }

我们现在可以给出获取存储方式的流程:
在这里插入图片描述

为什么最大容量要为2的次幂?

其实一部分原因之前的内容已经讲到。
一、当最大容量为2的次幂的时候,根据我们之前的说法,我们确定Index的时候会对hash值和length-1进行与操作,length-1右边为全1,我们每次扩容都是以两倍进行扩容,这样子就能保证每次扩容后有一半的几率索引不会改变,即使改变也只有一位之差(原因上面已经交代)。
二、我们可以想象一下,如果不为2的次幂,也就无法保证右边部分为全1,假设我们将hash值和1101进行与操作,我们可以想象,1101的第三位因为是0的原因,会造成这一位进行与操作的结果一定也是0,这样一定会造成一部分空间无法使用,因为与操作结果永远不可能出现在该位为1的索引上。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
int main(int argc, char *argv[]) { int i = 0; bool bOnce = true; char szPath[RH_MAX_PATH]; char szAbsPath[RH_MAX_PATH]; char szOrgPath[RH_MAX_PATH]; char szTemp[RH_MAX_BUFFER]; int iErrorCode = 0; CHashManager hashmgr; getcwd(szOrgPath, RH_MAX_PATH); //No arguments? if(argc == 1) { printInfo(); return(RH_NO_ARGS); } memset(szPath, 0, RH_MAX_PATH); bOnce = true; hashmgr.SelectAllAlgorithms(true); for(i = 1; i = RH_MAX_BUFFER) continue; // Non-parsable option argument, ignore fmtArgument(argv[i], szTemp); // Format the argument, i.e. remove all special chars if(strcmp(szTemp, "help" ) == 0) printInfo(); if(strcmp(szTemp, "h" ) == 0) printInfo(); if(strcmp(szTemp, "?" ) == 0) printInfo(); if(strcmp(szTemp, "version" ) == 0) printInfo(); if(strcmp(szTemp, "v" ) == 0) printInfo(); if(strcmp(szTemp, "fullpath") == 0) hashmgr.SetOption(OPT_FULLPATH, true); if(strcmp(szTemp, "f" ) == 0) hashmgr.SetOption(OPT_FULLPATH, true); if(strcmp(szTemp, "nopath" ) == 0) hashmgr.SetOption(OPT_FULLPATH, false); if(strcmp(szTemp, "rcrsv" ) == 0) hashmgr.SetOption(OPT_RECURSIVE, true); if(strcmp(szTemp, "norcrsv" ) == 0) hashmgr.SetOption(OPT_RECURSIVE, false); if(strcmp(szTemp, "recur" ) == 0) hashmgr.SetOption(OPT_RECURSIVE, true); if(strcmp(szTemp, "norecur" ) == 0) hashmgr.SetOption(OPT_RECURSIVE, false); if(strcmp(szTemp, "r" ) == 0) hashmgr.SetOption(OPT_RECURSIVE, true); if(strcmp(szTemp, "all" ) == 0) hashmgr.SelectAllAlgorithms(true); if(strcmp(szTemp, "a" ) == 0) hashmgr.SelectAllAlgorithms(true); if(strcmp(szTemp, "none" ) == 0) hashmgr.SelectAllAlgorithms(false); if(strcmp(s

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值