HashMap中的hash与rehash

HashMap中的hash与rehash

我们知道HashMap中经常用到hash()方法。

比如:put()方法中

public V put(K key, V value){
        if(key==null)
            return putForNullKey(value) ;
        int hash=hash(key.hashCode());   
        int i=indexfor(hash,table.length);
        for (Entry e=table[i];e!=null;e=e.next){
            Object k;
            if(e.hash==hash &&((k=e.key)==key ||key.equals(k))){
                V oldValue=e.value;
                e.value=value;
                e.recordAccess(this);
                return oldValue;
            }
        }
        modCount++;
        addEntry(hash,key,value,i);
        return null;
    }

我们知道hashCode是一个与元素地址有关的值,具体怎么计算的不需要关心,简单讲就是通过hashCode()方法

将一个对象转换成了一个数,中间经过了一系列运算。

但是HashMap中为什么有多了一个hash方法呢,对key的hashcode()方法的返回值又做了什么操作?

static final int hash(Object key){
                int h;
                return (key==null)?0:(h=key.hashCode())^(h>>>16);  //进行了右移16位的操作(高16位与低16位互换)
             }

HashMap中的hash与rehash_第1张图片

图片来源知乎

通过计算hash Code()与调用hash方法,就是要达到尽量少的key碰撞,最好是将(key,value)对均匀分配到

table数组中,减少put()和get()等操作的消耗的时间。

HashMap的初始化大小为16,然后每次扩容都是乘以2,这样设计也是有道理的。

HashMap中table[0]处存放的是key为null的键值对,除去这个特例,就剩下15个table位置。

15的二进制写法为1111。我们可以看index for()方法

 static int indexFor(int h,int length){
                return h&(length-1);
             }

通过计算hash()方法得到的值与table的大小减1相与(减的一就是为key=null留的)

我们知道0,1值与1相与的结果只与自身有关,与一个全1的数相与,可以完全体现这个数的特性。

如果我们不选择16或者2的n次方作为table的大小。举个例子:

假设一table大小为15,15-1=14。14的二进制表示为1110,我们可以看出不管一个数的最后一位为什么,相与之后都为0,

这样就相当于忽略了这一位的作用,而1111就不同了,它能够完全显示数本身的特性。

扩容时乘2操作能够继续保持这种特性。



rehash与HashMap的扩容有关,先看一下源码:

void resize(int newCapacity) {     
        Entry[] oldTable = table;     
        int oldCapacity = oldTable.length; 
        if (oldCapacity == MAXIMUM_CAPACITY) {    //判断容量是否已经超过了最大值
            threshold = Integer.MAX_VALUE;     //容量最大只能为Integer。MAX_VALUE.
            return;
        }

        Entry[] newTable = new Entry[newCapacity];   //创建一个新的数组
        transfer(newTable);    //将旧数组放到新的里面                        
        table = newTable;                             
        threshold = (int) (newCapacity * loadFactor);  //更新扩容的阈值
    }

详细看一下transfer()方法:

void transfer(Entry[] newTable) {
        Entry[] src = table;  //引用原table                   
        int newCapacity = newTable.length;   
        for (int j = 0; j < src.length; j++) {   //遍历原table 
            Entry e = src[j];                
            if (e != null) {   //如果原table[j]处不为null,即存在要复制的元素
                src[j] = null;     将原table[j]标记为null,等待GC回收
                do {   //这个while循环作用时为该处的链表中的所有Entry 在新的table中找到相应的位置
                    Entry next = e.next;
                    int i = indexFor(e.hash, newCapacity);    //计算在新的table中的下标
                    e.next = newTable[i];   
                    newTable[i] = e;        
                    e = next;               
                } while (e != null);
            }
        }
    }

HashMap是线程不安全的,为什么不安全,哪些操作会导致线程不安全呢?

下一篇文章我会详细分析一下HashMap中的一些可能导致线程不安全的操作。


你可能感兴趣的:(Java)