hash值的计算及HashMap的get(),put(),remove()方法解析

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

    1.1 为什么要hash = h ^ (h >>> 16)?

如果不做此操作,计算槽号:

(2^n - 1) ^ hashcode:

导致只有低n位参与运算。如果高16位改变,但是计算出来的hash没有任何改变。

所以,如果多个hashcode的低n位相同,而高位不同,得到的槽号相同,这使得hash表的散列不均匀,所以,为了让高16位也参与运算

h >>> 16:算术右移16位,得到的是原来的高16位变为低16位,高16位补0。

由此高16位可以和低16位运算。

 1. 2 为什么是异或操作?

为了保留高16位和低十六位的特征(为1的位)。当高16位或者低16位改变时,hash值可以跟着它改变,比如,高位从 1 变成0,低位为0时,异或结果由1变0;低位为1时,结果由0变1,使hash表更灵敏。

异或(^)是为 0,1的情况得到1,& 为1,1时得到1,它对高位或者低位的变化反应不灵敏,低位位0时,高位由1变0,hash对应的值仍为0,没有发生变化;只有低位为1时,高位发生变化,hash也跟着变,但是这不是我们想看到的。我们想要是只要高位或者低位发生变化,hash值就会改变。

那或操作呢?和 & 是一个情况啊。或操作是为0,0时得到0,比如,低位为1,高位由0变1,得到的结果永远是1,反应也不灵敏。所以我们采用异或操作。这样高16位和低16位发生改变时,我们可以得出不同的hash值,这样充分发挥了每个hashcode的高16位和低16位的特性,使得根据其计算出来的槽号尽可能不同,使它们均匀地分在hash表中,增大了hash表的散列程度。

1.3 槽位数为什么是 2^n?

为2^n时,槽位数-1 得到的低n位全部为1,进行与运算时,保留下来的是hash的低n位,如果不是2^n次方,比如为2^n+1,槽位数-1,低n位全为0,进行与运算得到的结果为低n+1位与的结果,该位要么为0要么为1,所以最后得到的槽位为0或2^n,所有hash运算的结果都得到这两个数字,所有结果都保留在这两个槽位中。

1.4 为什么是 (2^n-1) & hash?

相当于对数组长度取余。

2. HashMap方法解析

 2.1 get()方法

    HashMap的key可以为空对象吗?

可以。一切对象都有hashcode,null也是对象,为空对象时,计算出的hash为0。

得到槽位 (2^n-1) & hash: 为0。

而hashmap在put时,

if ((p = tab[i = (n - 1) & hash]) == null)
    tab[i] = newNode(hash, key, value, null);

为其new一个Node对象,hash为0,key,value为null,此时table[0]存了一个新的节点。

hashmap的value也可以是null。

那key为null,containsKey(key)得到什么呢?

public boolean containsKey(Object key) {
    return getNode(hash(key), key) != null;
}
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) {
                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;
    }

containsKey(key)调用getNode()方法,getNode方法看前七行就好,从table[0]获得Node对象,此对象不为空,因为已经为它在堆中分配内存和初始化,只是初始化的key和value都为null,所以getNode返回一个不为空的对象,所以containsKey(key)返回true。

为什么重写hashCode()方法?

        不重写,Object的hashcode默认根据地址值生成,只要new两个对象,那么得到的哈希值一定不同,这不是我们想要看到的。

         hashmap保留元素的前提是key对象的属性值不能相同,而不是地址。如果不重写equals方法,默认使用Object对象的equals方法,比较的是地址。

public boolean equals(Object obj) {
        return (this == obj);
    }

          首先要根据hash计算槽位,其次找到位置,如果该位置由元素,要判断hash值是否相同,由于会产生哈希碰撞,还要判断key的值是否相同,key值如果为基本数据类型,用==判断,为引用数据类型,用equals判断,所以key对象的equals方法要进行重写,比较属性值是否相同。

执行流程:

2.2 HashMap各阈值

容量:初始数组容量为16,阈值为0.75,当负载 > 16*0.75时,要将数组扩容为原来的2倍,即32

扩容也有长度限制:

static final int MAXIMUM_CAPACITY = 1 << 30;

当超过此值,不能扩容。

int有32位,最高位代表符号位,所以容量最大值为 2^30

树级阈值:

当链表长度大于8 并且 hashMap的元素值至少是64时,将其自动转化为红黑树,在此之前都将是数组+链表形式。

链表阈值:

当桶中元素 < 6,自动转化成链表

HashMap的每次操作都要new一个新数组(String同理)

2.3 put() 方法

源码:


//添加元素三种情况:
//  1.数组位置是null
//  2.数组位置不是null,key不重复,挂在链表或者红黑树上
//                      key重复,覆盖

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;
		//索引
		int i;
		
		tab = table;  //第一次赋值过来的为null
		
		n = tab.length;
		
        if (tab == null || n == 0){
		//  1.第一次添加数据,创建一个长16,加载因子为0.75的数组
		//  2.不是第一次,看数组是否达到了扩容条件;是扩容两倍,将数据移交到新的哈希表,否不处理
		    tab = resize();
			//将当前数组长度赋值给n
            n = tab.length;
		}
		
		//计算出当前键值对对象在数组中应存在的位置
			i = (n - 1) & hash;
			p = tab[i];
		
		//数组位置是null	
        if (p == null)
		//创建键值对对象,直接放到数组     拿key计算哈希值
            tab[i] = newNode(hash, key, value, null);
			
		//数组位置不为null
        else {
            Node<K,V> e; K k;
			
			//首先判断数组位置的元素与待添加元素hash是否同    键值重复
            if (p.hash == hash &&
                ((k = p.key) == key || (key != null && key.equals(k))))   // ?两次是否相同
                e = p;  //相同就把当前数组位置的节点赋给e
			
			// 键值不重复,要插入
			//红黑树节点
            else if (p instanceof TreeNode)
                e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
            
			//链表节点   如果键值重复,记录下重复的节点
			else {
                for (int binCount = 0; ; ++binCount) {

                    //获取数组元素的下一个节点	
					e = p.next;
                    if (e == null) {
                        p.next = newNode(hash, key, value, null);
						
						//添加完之后看是否需要调整
						//判断当前链表长度是否超过8
						//treeifyBin判断数组长度是否大于64,同时满足将链表转化为红黑树
                        if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
                            treeifyBin(tab, hash);
                        break;
                    }
					//key重复,不添加到链表了
                    if (e.hash == hash &&
                        ((k = e.key) == key || (key != null && key.equals(k))))
                        break;
                    p = e;  //下次循环
                }
            }
			// e为null,无需覆盖元素
            if (e != null) { // existing mapping for key
                V oldValue = e.value;
                if (!onlyIfAbsent || oldValue == null)
				    //等号右边为要添加的值   覆盖:value进行覆盖,原先的键值对还在
                    e.value = value;
                afterNodeAccess(e);
                return oldValue;
            }
        }
        ++modCount;
		// thredshold:n*0.75   扩容
        if (++size > threshold)
            resize();
        afterNodeInsertion(evict);
		
		// 当时没有覆盖任何元素
        return null;
    }
	

 流程图:

 

2.4 remove()方法

源码:

// 删除指定key的键值对,返回被删除的键值对的值
	// 返回为空,key可能不存在,或key为null

	public V remove(Object key) {
        Node<K,V> e;
        return (e = removeNode(hash(key), key, null, false, true)) == null ?
            null : e.value;
    }
	
	
	
	// value是否删除,取决于 matchValue,为true时,当key和value都相等时才会删除,false则不管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;
        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;
            else if ((e = p.next) != null) {
			//树节点
                if (p instanceof TreeNode)
                    node = ((TreeNode<K,V>)p).getTreeNode(hash, key);
				//链表节点	
                else {
                    do {
                        if (e.hash == hash &&
                            ((k = e.key) == key ||
                             (key != null && key.equals(k)))) {
							//找到了  break循环 
                            node = e;
                            break;
                        }
                        p = e;
                    } while ((e = e.next) != null);
                }
            }
			
			
			// node为找到的与key匹配的目标节点
			// !matchValue : 当matchValue为false时,代表不需要关心值,判断短路,不走后面
			//                              true,   要比较值了,继续走后面的判断
            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);
				//找到node了,node=p时,代表当前槽的节点是node(链表或者树的第一个节点)
                else if (node == p)
                    tab[index] = node.next;
				// 否则,p为目标节点的前一个节点,删除node节点	
                else
                    p.next = node.next;
                ++modCount;
                --size;
                afterNodeRemoval(node);
				//返回被删除的节点
                return node;
            }
        }
		// 没有找到节点,返回null
        return null;
    }

执行流程:

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值