面试之HashMap

152 阅读4分钟

HashMap数据结构

HashMap是一种数组加链表的数据结构。基于Map接口方式实现,元素以键值对的方式存储,允许null为key,key不允许重复,无序,线程不安全。

static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16 默认容量

static final float DEFAULT_LOAD_FACTOR = 0.75f;     //负载因子0.75

如图:



存取过程

put

public V put(K key, V value) {   
 if (table == EMPTY_TABLE) {  
      inflateTable(threshold);  
 }  
 if (key == null)    
    return putForNullKey(value);   
 int hash = hash(key);    
 int i = indexFor(hash, table.length); 
 for (Entry<K,V> 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;
}

put的时候先判断数组容量,如果是0就初始化数组。数组默认大小为16,当然hashMap提供一个有参数的构造方法来指定数组容量。注意 ,并不是容量并不由我们指定的算,eg:当你指定容量是6,那么会生成一个为8的数组,如果思20,就会生成一个32的数组,也就是你想初始一个大小为n的数组,HashMap会初始化一个大小 大于等于N的二次方的一个数组。

如果key是null,就把改元素放到一个指定的位置。

计算要存放的数组位置: 计算key的hash值,用key的hash和数组的size 做 & 运算

static int indexFor(int h, int length) {
    // assert Integer.bitCount(length) == 1 : "length must be a non-zero power of 2";
    return h & (length-1);
}

按位与运算符(&)

参加运算的两个数据,按二进制位进行“与”运算. 两位同时为“1”,结果才为“1”,否则为0

eg:  3&5 即 0000 0011& 0000 0101 = 00000001 因此,3&5的值得1。

为什么计算数组下标要用 与 操作

因为把任意长度的字符串变成固定长度的字符串,所以存在一个hash对应多个字符串的情况,所以碰撞必然存在

为了减少hash值的碰撞,需要实现一个尽量均匀分布的hash函数,在HashMap中通过利用key的hashcode值,来进行位运算
公式:index = e.hash & (newCap - 1)

所以我们发现,如果我们想把HashCode转换为覆盖数组下标取值范围的下标,跟我们的length是非常相关的,length如果是16,那么减一之后就是15(0000 1111),正是这种高位都为0,低位都为1的二级制数才保证了可以对任意一个hashcode经过逻辑与操作后得到的结果是我们想要的数组下标。这就是为什么在真初始化HashMap的时候,对于数组的长度一定要是二次方数,二次方数和算数组下标是息息相关的,而这种位运算是要比取模更快的。


如果hash冲突  以链表的方式进行存储。将新节点插在链表的头部,此时新节点就是当前这个链表的头节点,接下来把头节点移动到数组位置即可。

如果key相同,value覆盖,返回新值

总结:初始化数组,,

  1. 如果数组为空,初始化数组
  2. 计算key的hash值,hash值和数组size-1做与运算 得到下标
  3. 遍历改下标下的链表,如果 key相同,value覆盖,返回旧值
  4. 将key,value组成Entry对象,将节点插入 index 链表的头部
  5. 将链条的头节点移动到数组上

get

public V get(Object key) {
    if (key == null)    
        return getForNullKey();    
    Entry<K,V> entry = getEntry(key);
    return null == entry ? null : entry.getValue();}

  1. 判断数组为0,直接返回空
  2. 计算key的hash值,hash值和数组size-1做与运算 得到下标
  3. 遍历改下标下的链表,比较key的hash,key值 返回entry对象

扩容

 数组容量扩大两倍,然后把元素 移到新 数组。eg  index = 1 ,链表 1 2 3 。

 移动 链表的时候有规律 在jdk8 会改动。


jdk1.8变动:

链表长度在8 的时候 会判断 数组的容量大于64吗,如果小于 会扩容 数组。

如果链表长度大于8 并且 容量大于64 会将链表改成 红黑树。

因为红黑树 存 取 的速度都比较均匀。